/*
Copyright (c) 2009-2010, Warwick Warp Limited
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the Warwick Warp Limited nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL WARWICK WARP LIMITED BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#ifndef WOPTIONS_OPTIONS_H_INCLUDED
#define WOPTIONS_OPTIONS_H_INCLUDED

#include <map>
#include <string>
#include <sstream>
#include <limits>
#include <vector>
#include <iostream>

#include "detail/boost_headers.h"
#include "argumentexception.h"
#include "option_binders.h"
#include "argument_binders.h"

/*!
	\brief Classes to support parsing command line options.
	Implementations in this scope use char as their underlying character type.
*/
namespace woptions {

//! \internal
namespace detail {

class UsageFormatter;
std::ostream& operator<<(std::ostream& os, const UsageFormatter& fmt);

//! \brief Class to assist in formatting usage
class UsageFormatter
{
public:
	UsageFormatter() :
		m_longest_preamble(0)
	{
	}
	
	//! \brief Specify an argument output without decoration
	void undecorated_argument(std::string label);
	//! \brief Automatically document an argument based on occurances
	void argument(std::string label, size_t min_count, size_t max_count);
	//! brief Describe an option
	void option(std::string label, std::string args, std::string desc, std::string def_value);

private:
	struct argument_s 
	{ 
		std::string label;
		size_t      min_count;
		size_t      max_count;
	};
	struct option_s
	{
		std::string preamble;
		std::string desc;
	};
	
	std::vector<argument_s> m_arguments;
	std::vector<option_s>   m_options;
	size_t                  m_longest_preamble;
	
	friend std::ostream& operator<<(std::ostream& os, const UsageFormatter& fmt);
};

} // namespace detail
	
/*!
	\brief Implementations agnostic of the underlying character type
*/
namespace basic {
	
/*!
	Generic class representing character specific behaviour for parsing command 
	line options.
	
	\ingroup traits
*/
template<typename char_t> 
struct options_traits
{
	//! \brief String type for the arguments
	typedef std::basic_string<char_t> arg_type;
	//! \brief Iterator type for iterating over flags in a flag list.
	typedef typename arg_type::const_iterator flag_iterator;
	
	//! Convert an argument to a narrow string for std::ostream and std::exception formatting
	static std::string to_char(const arg_type& s)
	{
		return std::string(s.begin(),s.end());
	}
	
	//! \return true if s is considered to be an option
	static bool is_option(const arg_type& s)
	{
		return s.length() >= 2 && s[0] == '-';
	}
	
	//! \return true if s could be a list of flags
	static bool is_flag_list(const arg_type& s)
	{
		return s.length() >= 2 && s[0] == '-' && s[1] != '-';
	}
	
	/*! 
		\brief split a parameterised option into its key and value components
		\return true if the key/value pair was successfully extracted
	*/
	static bool split(const arg_type& arg, arg_type& key, arg_type& value)
	{
		if(arg.length() >= 2)
		{
			size_t eqpos = arg.find('=');
			if(eqpos != arg_type::npos)
			{
				key   = arg.substr(0,eqpos);
				value = arg.substr(eqpos+1);
			}
			else
			{
				key   = arg.substr(0,2);
				value = arg.substr(2);
			}
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/*!
		\brief Iterate over flags in a flag_list
	*/
	static flag_iterator begin_flags(const arg_type& arg)
	{
		if(arg.size() >= 2)
			return arg.begin()+1;
		else
			return arg.end();
	}
	
	/*!
		\brief Iterate over flags in a flag_list
	*/
	static flag_iterator end_flags(const arg_type& arg)
	{
		return arg.end();
	}
	
	/*!
		\brief Turn the a dereferenced flag iterator into the corresponding 
		option string for the flag.
	*/
	static arg_type make_flag_option(char_t f)
	{
		arg_type s(2,char_t());
		s[0] = '-';
		s[1] = f;
		return s;
	}
};
	
/*!
	\brief Base class for parsing options with a generic character type

	For more detail on the requirements of traits_t see the options_traits 
	class.

	\ingroup major
*/
template<typename char_t, typename traits_t=options_traits<char_t> >
class Options
{
public:
	//! \brief Traits specification for this Options class
	typedef traits_t traits_type;
	//! \brief String type for arguments
	typedef typename traits_type::arg_type arg_type;
	
	//! \brief The group ID assigned to options by default
	static const size_t DEFAULT_GROUP = 0;

	/*!
		\brief Initialise an options object that takes no arguments other than
		options
	*/
	Options()
	{
		group_caption(DEFAULT_GROUP,"Options");
	}
	
	virtual ~Options() {}
	
	/*!
		\brief Set the caption of a function group
	*/
	void group_caption(size_t index, const std::string& desc)
	{
		if(index >= m_function_groups.size())
			m_function_groups.resize(index+1);
		m_function_groups[index] = desc;
	}
	
	/*!
		\brief Register an argument handler
	*/
	template<typename BINDER>
	void argument(std::string   format, //!< Descriptive format text for the argument
	              BINDER binder         //!< Function used to handle the argument, must be derived from bind_argument and copy constructable
				  )
	{
		arguments(format,1,1,binder);
	}
	
	/*!
		\brief Register an argument handler that can handle a variable number of 
		arguments
		
		Like a regular expression, each specification consumes as many arguments 
		as possible whilst generating a valid combination.
	*/
	template<typename BINDER>
	void arguments(std::string format,  //!< Descriptive format text for the argument
				   size_t min_count,    //!< Minimum number of occurances
				   BINDER binder        //!< Function used to handle the argument, must be derived from bind_argument and copy constructable
				   )
	{
		arguments(format,min_count,std::numeric_limits<size_t>::max(),binder);
	}
	
	/*!
		\brief Register an argument handler that can handle a variable number of 
		arguments
		
		Like a regular expression, each specification consumes as many arguments 
		as possible whilst generating a valid combination.
	*/
	template<typename BINDER>
	void arguments(std::string format,  //!< Descriptive format text for the argument
				   size_t min_count,    //!< Minimum number of occurances
				   size_t max_count,    //!< Maximum number of occurances
				   BINDER binder        //!< Function used to handle the argument, must be derived from bind_argument and copy constructable
				   )
	{
		if(format.empty())
			throw ConfigurationException("empty format");
		if(max_count<min_count)
			throw ConfigurationException("max_count must be greater than or equal to min_count");

		arg_info_s arg;
		arg.min_count = min_count;
		arg.max_count = max_count;
		arg.format    = format;
		arg.binder    = boost::shared_ptr< bind_argument<char_t> >(new BINDER(binder));
		
		m_arg_info.push_back(arg);
	}
	
	/*!
		\brief Register an option
	*/
	template<class BINDER>
	void option(arg_type    opt,    //!< The option string
	            std::string format, //!< Descriptive format text for the option
	            std::string desc,   //!< Description of the option's purpose
	            BINDER binder,      //!< Class used to handle the option, must be derived from bind_option and copy constructable
	            size_t group=DEFAULT_GROUP //!< Index of the function group
	            )
	{
		if(!traits_type::is_option(opt))
			throw ConfigurationException("invalid option string");
		if(format.empty() && binder.num_args() > 0)
			throw ConfigurationException("option has arguments but format is empty");
		
		opt_info_s info;
		info.format      = format;
		info.description = desc;
		info.binder      = boost::shared_ptr< bind_option<char_t> >(new BINDER(binder));
		info.group       = group;
		m_opt_info.insert(std::make_pair(opt,info));
		
		if(group >= m_function_groups.size())
			m_function_groups.resize(group+1);
	}
	
	/*!
		\brief Parse command line arguments
		\throws ArgumentException if an error occurs
	*/
	void parse(int argc, char_t *argv[])
	{
		std::vector<arg_type> arg_vector(argc);
		for(int i=0;i<argc;++i)
			arg_vector[i] = argv[i];
		parse(arg_vector);
	}
	
	/*!
		\brief Parse command line arguments
	
		All the other parse calls map to this one, so override this one if 
		desired.
		
		\throws ArgumentException if an error occurs
	*/
	virtual void parse(const std::vector<arg_type>& args)
	{
		size_t num_arguments       = count_args(args);
		std::vector<size_t> binder = assign_arg_binders(num_arguments);
		
		assert(binder.size() == num_arguments);
		
		// Now actually call all the binders in the order the arguments were 
		// presented
		size_t current_arg = 0;
		for(size_t i=0;i<args.size();++i)
		{
			if(traits_type::is_option(args[i]))
			{
				typename std::map<arg_type,opt_info_s>::iterator it = m_opt_info.find(args[i]);
				if(it != m_opt_info.end())
				{
					if(i+it->second.binder->num_args()>=args.size())
						throw ArgumentException(std::string("Missing argument for option ")+traits_type::to_char(args[i]));
					
					std::vector<arg_type> opt_args(it->second.binder->num_args());
					for(size_t j=0;j<it->second.binder->num_args();++j,++i)
						opt_args[j] = (args[i+1]);
					(*it->second.binder)(it->first,opt_args);
				}
				else
				{
					// Parsing for special types of option
					if( !handle_opt_with_parameter(args[i]) &&
						!handle_flags(args[i]) )
					{
						throw ArgumentException(std::string("Unrecognised option: ")+traits_type::to_char(args[i]));
					}
				}
			}
			else if(binder[current_arg] < m_arg_info.size())
			{
				(*m_arg_info[binder[current_arg]].binder)(args[i]);
				++current_arg;
			}
			else
			{
				throw ArgumentException(std::string("Excess argument: ")+traits_type::to_char(args[i]));
			}
		}
	}
	
	//! \brief Report usage to an ostream.
	void usage(
		arg_type arg0=arg_type(),            //!< Name of the program (args[0])
		size_t function_group=DEFAULT_GROUP, //!< Function group to output usage for
		std::ostream& to=std::cout           //!< Output stream
		) const
	{
		if( function_group == DEFAULT_GROUP )
		{
			detail::UsageFormatter fmt;
			
			if(!arg0.empty())
				fmt.undecorated_argument(traits_type::to_char(arg0));
			if(m_opt_info.size() > 0)
				fmt.undecorated_argument("[options]");
			// NB: arg0 is handled above!
			for(size_t i=arg0.empty()?0:1;i<m_arg_info.size();++i)
			{
				fmt.argument(m_arg_info[i].format,m_arg_info[i].min_count,m_arg_info[i].max_count);
			}
			
			to << "Usage: " << fmt;
		}
		
		{
			detail::UsageFormatter fmt;
			size_t num_options = 0;
			for(typename std::map<arg_type,opt_info_s>::const_iterator it=m_opt_info.begin();
				it!=m_opt_info.end();
				++it)
			{
				if(it->second.group == function_group)
				{
					fmt.option(
						traits_type::to_char(it->first),
						it->second.format,
						it->second.description,
						it->second.binder->default_string());
					++num_options;
				}
			}
			
			if(num_options > 0)
			{
				if( function_group < m_function_groups.size() && 
					!m_function_groups[function_group].empty())
					to << m_function_groups[function_group] << ":" << std::endl;
				to << fmt;
			}
		}
	}

private:
	/* 
		These options are safe on the Options class, but are unlikely to be 
		safe on derived classes because binders store addresses.
		
		We could fix this by implementing these, but refusing to copy binders 
		thus forcing any derived classes to reinitialise their binder-tables
		on copy.
	*/
	Options(const Options&);
	Options& operator=(const Options&); 

	size_t count_args(const std::vector<arg_type>& args) const
	{
		size_t num_arguments = 0;
		for(size_t i=0;i<args.size();++i)
		{
			if(traits_type::is_option(args[i]))
			{
				typename std::map<arg_type,opt_info_s>::const_iterator it = m_opt_info.find(args[i]);
				if(it != m_opt_info.end())
				{
					i += it->second.binder->num_args();
				}
			}
			else
			{
				++num_arguments;
			}
		}
		return num_arguments;
	}
	
	bool handle_opt_with_parameter(const arg_type& arg)
	{
		// Handle single character options with one argument in the form
		// -X(value) or -option=value
		arg_type key;
		arg_type value;
		if( traits_type::split(arg,key,value) )
		{
			typename std::map<arg_type,opt_info_s>::iterator it = m_opt_info.find(key);
			if( it != m_opt_info.end() && it->second.binder->num_args() == 1)
			{
				std::vector<arg_type> opt_args(1,value);
				(*it->second.binder)(it->first,opt_args);
				return true;
			}
			else
			{
				return false;
			}
		}
		else
		{
			return false;
		}
	}
	
	bool handle_flags(const arg_type& arg)
	{
		// Argument must have some options and not start "--"
		if(!traits_type::is_flag_list(arg))
			return false;
		
		// Handle single character options with no arguments ("flags")
		for(typename traits_type::flag_iterator flag = traits_type::begin_flags(arg);
			flag != traits_type::end_flags(arg);
			++flag)
		{
			arg_type opt = traits_type::make_flag_option(*flag);
			typename std::map<arg_type,opt_info_s>::iterator it = m_opt_info.find(opt);
			if( it != m_opt_info.end() && it->second.binder->num_args() == 0)
			{
				std::vector<arg_type> opt_args;
				(*it->second.binder)(it->first,opt_args);
			}
			else
			{
				return false;
			}
		}
		
		return true;
	}
	
	std::vector<size_t> assign_arg_binders(size_t num_arguments)
	{
		std::vector<size_t> binder(num_arguments, m_arg_info.size());
		
		// Count the number of required arguments after the ith argument
		std::vector<size_t> min_arguments(m_arg_info.size(), 0);
		if(m_arg_info.size() > 0)
		{
			min_arguments[m_arg_info.size()-1] = 0;
			for(size_t i=m_arg_info.size()-1;i>0;--i)
				min_arguments[i-1] = min_arguments[i] + m_arg_info[i].min_count;
			
			if(num_arguments < min_arguments[0]+m_arg_info[0].min_count)
				throw ArgumentException("Insufficient arguments.");
		}

		size_t current_binder  = 0;
		size_t num_binder_args = 0;
		for(size_t i=0;i<num_arguments && current_binder<m_arg_info.size();++i)
		{
			while(
				current_binder < m_arg_info.size() && (
					num_binder_args >= m_arg_info[current_binder].max_count || 
					i+min_arguments[current_binder] >= num_arguments
				))
			{
				// Advance the binder while either all arguments have been 
				// assigned, or the subsequent binders require all the remaining
				// arguments
				++current_binder;
				num_binder_args = 0;
			}
			
			++num_binder_args;
			binder[i] = current_binder;
		}
		
		return binder;
	}

	struct arg_info_s
	{
		arg_info_s() : min_count(0), max_count(0) {}
		
		size_t min_count;
		size_t max_count;
		std::string format;
		boost::shared_ptr< bind_argument<char_t> > binder;
	};
	
	struct opt_info_s
	{
		opt_info_s() :  group(DEFAULT_GROUP) {}
		
		size_t group;
		std::string format;
		std::string description;
		boost::shared_ptr< bind_option<char_t> > binder;
	};
	
	std::vector<std::string> m_function_groups;
	std::vector<arg_info_s>  m_arg_info;
	std::map<arg_type,opt_info_s> m_opt_info;
};
	
} // namespace basic

/*!
	\brief Implementations using wchar_t as the underlying character type
*/
namespace wide {
	
/*!
	\brief Wide character options class
	\ingroup major
*/
typedef basic::Options<wchar_t> Options;
	
} // namespace wide
	
/*!
	\brief Base class for parsing options
	\ingroup major
*/
typedef basic::Options<char> Options;

} // namespace woptions

#endif

